/*
 * ﻿Copyright (C) 2012-2013 NewMain Softech
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package com.newmainsoftech.ant.script;

import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.tools.ant.Project;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ReadMultiLineArgs {
	private Logger logger = LoggerFactory.getLogger( this.getClass()); 
		protected Logger getLogger() {
			return logger;
		}

	public static enum ArgPrefix {
		Minus( "-"), Space( " ");
		
		private final String prefix;
			public String getPrefix() {
				return prefix;
			}
		private ArgPrefix( String prefix) {
			this.prefix = prefix;
		}
	}
	
	private String preFixStr;
		/**
		 * Return prefix string being used in parsing multi-line argument.
		 * 
		 * @return prefix string, it will be used in parsing multi-line argument. 
		 */
		public String getPreFixStr() {
			return preFixStr;
		}
		/**
		 * Set prefix string being used as delimiter of each argument in parsing multi-line argument.
		 * @param preFixStr
		 */
		public void setPreFixStr( final String preFixStr) {
			this.preFixStr = preFixStr;
		}

	public static final String RegExpStrForBlankSpacePrefix 
	= "(^(\\S|\\e)+)|([ \\t\\n\\r\\f]+(\\S|\\e)+)";
		public static String getRegExpStrForBlankSpacePrefix() {
			return RegExpStrForBlankSpacePrefix;
		}

	public static final String RegExpEscapedChar = "$^.*+?[]\\{}";
		// characters were quoted from http://java.sun.com/developer/technicalArticles/releases/1.4regex/
	private String regExpStr;
		/**
		 * Return regular expression string being used in parsing multi-line argument. <br />
		 * When custom value has not been set via {@link #setRegExpStr(String)}:
		 * <ul>
		 * <li>if prefix string has not been set by {@link #setPreFixStr(String)} method, 
		 * then this will use default regular expression string for blank space delimiter what 
		 * can be obtained by {@link #getRegExpStrForBlankSpacePrefix()} method.
		 * </li>
		 * <li>if prefix string has been set, then it constructs a regular expression on the fly 
		 * considering the occurrence of either one of next patterns as an argument (using '-' character 
		 * as prefix for the description):
		 * <ul>
		 * <li>-argValue</li>
		 * <li>-argName argValue</li>
		 * <li>arg name followed by other delimiter string than prefix string and arg value without 
		 * space or tab between them</li>
		 * </ul> 
		 * </li>
		 * </ul>
		 * @return regular expression string being used for analyze multi-line argument
		 */
		public String getRegExpStr() {
			Logger logger = getLogger();
			
			if ( regExpStr != null) {
				if ( logger.isDebugEnabled()) {
					logger.debug( 
							String.format(
									"Returing regular expression: %1$s",
									regExpStr
									)
							);
				}
				return regExpStr;
			}
			
			String preFixStr = getPreFixStr();
				if ( preFixStr == null) {
					if ( logger.isDebugEnabled()) {
						logger.debug( 
								String.format(
										"Returing regular expression: %1$s",
										ReadMultiLineArgs.getRegExpStrForBlankSpacePrefix()
										)
								);
					}
					return ReadMultiLineArgs.getRegExpStrForBlankSpacePrefix();
				}
				if ( preFixStr.trim().length() < 1) {
					if ( logger.isDebugEnabled()) {
						logger.debug( 
								String.format(
										"Returing regular expression: %1$s",
										ReadMultiLineArgs.getRegExpStrForBlankSpacePrefix()
										)
								);
					}
					return ReadMultiLineArgs.getRegExpStrForBlankSpacePrefix();
				}
				
			String preFixStrCopy = "";
				for( int index = 0; index < preFixStr.length(); index++) {
					if ( ReadMultiLineArgs.RegExpEscapedChar
							.contains( preFixStr.substring( index, index + 1))) {
						preFixStrCopy = preFixStrCopy + "\\" + preFixStr.substring( index, index + 1);
					}
					else {
						preFixStrCopy = preFixStrCopy + preFixStr.substring( index, index + 1);
					}
				} // for
				
			String regExpLocalStr 
			= "[ \\t]*" + preFixStrCopy + "([\\S\\e]+)([ \\t]+(?!" + preFixStrCopy + ")" + "[\\S\\e]*)?";
			if ( logger.isDebugEnabled()) {
				logger.debug( 
						String.format(
								"Returing regular expression: %1$s",
								regExpLocalStr
								)
						);
			}
			
			return regExpLocalStr;
		}
		/**
		 * Set regular expression string being used for analyze multi-line argument
		 * @param regExpStr
		 */
		public void setRegExpStr( final String regExpStr) {
			this.regExpStr = regExpStr;
		}

	ArrayList<String> split( final String multilineArgs, final String regExpStr) {
		final Pattern pattern = Pattern.compile( regExpStr, Pattern.MULTILINE);
		final Matcher matcher = pattern.matcher( multilineArgs);
		final ArrayList<String> argList = new ArrayList<String>();
		int preStartIndex = 0;
		int preEndIndex = 0;
		while( matcher.find()) {
			int startIndex = matcher.start();
			int endIndex = matcher.end();
			String token = matcher.group();
			
			if ( startIndex < preEndIndex) {
			// case like delimited by space in the middle of quoted string.
				if ( endIndex <= preEndIndex) { 
					continue; // while
				}
				else {
				// This case is really irregular and strange: there is still continuous string not 
				// delimited by white space(s) after closing quotation character. 
					argList.set( 
							argList.size() - 1,
							multilineArgs.substring( preStartIndex, endIndex)
							);
					preEndIndex = endIndex;
					continue; // while
				}
			}
			else {
				// When previous match left some residue string, then append it to last match ---------
				String residue = multilineArgs.substring( preEndIndex, startIndex).trim();
					if ( residue.trim().length() > 0) {
						argList.set( 
								argList.size() - 1, 
								multilineArgs.substring( preStartIndex, startIndex).trim());
					}
				// ------------------------------------------------------------------------------------
					
				int quotationCharCount = token.length() - token.replace( "\"", "").length();
				if ( (quotationCharCount % 2) != 0) {
					// Locate matching closing quotation
					int closingQuotationIndex = multilineArgs.indexOf( "\"", endIndex);
						if ( closingQuotationIndex >= 0) {
							endIndex = closingQuotationIndex + 1;
							token = multilineArgs.substring( startIndex, endIndex);
						}
						/* Broken quotation case; do nothing about it and let it be.
						else {
						}
						 */
				}
				
				preStartIndex = startIndex;
				preEndIndex = endIndex;
					
				argList.add( token.trim());
			}
		} // while
		if ( argList.size() > 0) {
			String residue = multilineArgs.substring( preEndIndex).trim();
			if ( residue.trim().length() > 0) {
				argList.set( 
						argList.size() - 1, 
						multilineArgs.substring( preStartIndex).trim());
			}
		}
		
		return argList;
	}
	
	/**
	 * Split string provided as multilineArgs input by using regular expression obtained by 
	 * {@link #getRegExpStr()}
	 * @param multilineArgs muli-line arguments
	 * @return list of arguments split by regular expression
	 */
	public ArrayList<String> splitByPrefix( final String multilineArgs) {
		if ( multilineArgs == null) {
			throw new IllegalArgumentException( "multilineArgs input cannot be null.");
		}
		if ( multilineArgs.trim().length() < 1) {
			Logger loggerObj = getLogger();
				if ( loggerObj.isInfoEnabled()) {
					loggerObj.info( 
							"Case of irregular inputs: multilineArgs input " 
							+ "provided only white-space(s) value." 
							);
				}
				 
			ArrayList<String> argList = new ArrayList<String>();
			return argList;
		}
		
		Logger logger = getLogger();
		if ( logger.isDebugEnabled()) {
			logger.debug( 
					String.format(
							"Going to parse multi-line argument string: [%1$s]", 
							multilineArgs)
					);
		}
		
		String regExpStr = getRegExpStr();
		ArrayList<String> argList = split( multilineArgs, regExpStr);
			if ( logger.isDebugEnabled()) {
				logger.debug( 
						String.format(
								"Result of parsing multi-line argument string: %1$s", 
								argList.toString())
						);
			}
		return argList;
	}

	/**
	 * Container class of regular expression being used to extract actual part 
	 * (excluding "${" and "}") of property name or reference ID out of give argument.
	 * 
	 * @author <a href="mailto:artymt@gmail.com">Arata Y.</a>
	 */
	public static class PropertyLocaterPatternContainer {
		/**
		 * Default regular expression to pick up "text" out of ${"text"} as a step of process of 
		 * resolving property or reference value. 
		 */
		public final String DefaultPropertyLocaterRegExp 
		= "[ \\t]*(\\$\\{[ \\t]*([\\S&&[^\\}]]+)[ \\t]*\\})";
		/**
		 * In {@link #DefaultPropertyLocaterRegExp}, group number for actual part 
		 * (excluding "${" and "}") of property name or reference ID.
		 */
		public final int DefaultGroupIndexForPropertyExp = 2;
		private final int groupIndexInPropertyExp;
			/**
			 * Return group index number what should be used with {@link Pattern} object returned by 
			 * {@link #getGroupIndexInPropertyExp()}
			 * @return group number for actual part (excluding "${" and "}") of property name 
			 * or reference ID
			 */
			public int getGroupIndexInPropertyExp() {
				return groupIndexInPropertyExp;
			}
		private final Pattern propertyLocaterPattern;
			/**
			 * Return {@link Pattern} object made with regular expression fed to constructor. 
			 * The returned <code>Pattern</code> object supposes to be being used to extract property 
			 * name or reference ID out of argument. 
			 * @return <code>Pattern</code> object made with given regular expression to constructor
			 */
			public Pattern getPropertyLocaterPattern() {
				return propertyLocaterPattern;
			}
		// Constructors ---------------------------------------------------------------------------	
		/**
		 * Construct using {@link #DefaultPropertyLocaterRegExp} 
		 * (and {@link #DefaultGroupIndexForPropertyExp})
		 */
		public PropertyLocaterPatternContainer() {
			propertyLocaterPattern = Pattern.compile( DefaultPropertyLocaterRegExp);
			groupIndexInPropertyExp = DefaultGroupIndexForPropertyExp;
		}
		/**
		 * Construct with provided string as regular expression to locate actual part 
		 * (excluding "${" and "}") of property name or reference ID. 
		 * 
		 * @param propertyLocaterRegExp regular expression to locate property name or reference ID
		 * @param groupForPropertyExp group index points actual part of property name or reference ID 
		 */
		public PropertyLocaterPatternContainer( 
				final String propertyLocaterRegExp, final int groupForPropertyExp) {
			propertyLocaterPattern = Pattern.compile( propertyLocaterRegExp);
			this.groupIndexInPropertyExp = groupForPropertyExp;
		}
		// ----------------------------------------------------------------------------------------
	}
	private PropertyLocaterPatternContainer propertyLocaterPatternContainer 
	= new PropertyLocaterPatternContainer();
		protected PropertyLocaterPatternContainer getPropertyLocaterPatternContainer() {
			return propertyLocaterPatternContainer;
		}
		protected void setPropertyLocaterPatternContainer( 
				PropertyLocaterPatternContainer propertyLocaterPatternContainer) {
			this.propertyLocaterPatternContainer = propertyLocaterPatternContainer;
		}
	
	String replaceDesignation( final String argStr, int startIndex, int endIndex, final String replacement) {
		String prefix = argStr.substring( 0, startIndex);
			prefix = prefix.substring( 0, prefix.lastIndexOf( "${"));
		String suffix = argStr.substring( endIndex);
			suffix = suffix.substring( suffix.indexOf( "}") + 1);
		String newArgStr = prefix + replacement + suffix;
		return newArgStr;
	}
	
	/**
	 * Replace Ant property name or reference ID in <code>rawToken</code> input to actual string value. 
	 * 
	 * @param rawToken
	 * @return string in what Ant property names or reference IDs are replaced with actual string value.  
	 */
	String parsePropertiesAndReferences( final String rawToken, final Project antProject) {
		final PropertyLocaterPatternContainer propertyLocaterPatternContainer
		= getPropertyLocaterPatternContainer();
		final Pattern pattern = propertyLocaterPatternContainer.getPropertyLocaterPattern();
		final int groupIndex = propertyLocaterPatternContainer.getGroupIndexInPropertyExp();
		
		String token = rawToken;
		Matcher matcher = pattern.matcher( token);
		while( matcher.find()) {
			int startIndex = matcher.start( groupIndex);
			int endIndex = matcher.end( groupIndex);
			String name = token.substring( startIndex, endIndex);
				String propertyValue = antProject.getProperty( name);
					if ( propertyValue != null) {
						token = replaceDesignation( token, startIndex, endIndex, propertyValue);
						matcher = pattern.matcher( token);
						continue; // while
					}
				Object referredObj = antProject.getReference( name);
					if ( referredObj != null) {
						token  
						= replaceDesignation( 
								token, startIndex, endIndex, referredObj.toString());
						matcher = pattern.matcher( token);
						continue; // while
					}
				if ( logger.isDebugEnabled()) {
					logger.debug( 
							String.format(
									"Could not find property or reference defined with name: %1$s. " 
									+ "Thereby, replacing that part in %2$s with an empty space.",
									name, token)
							);
				}
				token  = replaceDesignation( token, startIndex, endIndex, "");
				matcher = pattern.matcher( token);
		} // while
		
		return token;
	}
	
	public ArrayList<String> splitByPrefix( final String multilineArgs, final Project antProject) {
		if ( multilineArgs == null) {
			throw new IllegalArgumentException( "multilineArgs input cannot be null.");
		}
		if ( multilineArgs.trim().length() < 1) {
			Logger loggerObj = getLogger();
				if ( loggerObj.isInfoEnabled()) {
					loggerObj.info( 
							"Case of irregular inputs: multilineArgs input " 
							+ "provided only white-space(s) value." 
							);
				}
				 
			ArrayList<String> argList = new ArrayList<String>();
			return argList;
		}
		
		Logger logger = getLogger();
		if ( logger.isDebugEnabled()) {
			logger.debug( 
					String.format(
							"Going to parse multi-line argument string: [%1$s]", 
							multilineArgs)
					);
		}
		
		// Parse property in multilineArgs input
		String multilineArgsCopy = parsePropertiesAndReferences( multilineArgs, antProject);
			if ( logger.isDebugEnabled()) {
				logger.debug( 
						String.format(
								"Multi-line argument string after resolving property and reference: [%1$s]", 
								multilineArgsCopy)
						);
			}
		String regExpStr = getRegExpStr();
		ArrayList<String> argList = split( multilineArgsCopy, regExpStr);
			if ( logger.isDebugEnabled()) {
				logger.debug( 
						String.format(
								"Result of parsing multi-line argument string: %1$s", 
								argList.toString())
						);
			}
		return argList;
	}
	
	private String concatinateStr;
		/**
		 * Return string being used as the end delimiter to concatenate split arguments to single line. <br />
		 * When custom value has not been set by {@link #setConcatinateStr(String)}}, then single blank 
		 * string will be returned as default value.
		 * @return String being used as the end delimiter to concatenate split arguments to single line.
		 */
		public String getConcatinateStr() {
			if ( ( concatinateStr == null) || "".equals( concatinateStr)) {
				Logger loggerObj = getLogger();
				if ( loggerObj.isInfoEnabled()) {
					loggerObj.info( 
							"Custom concatinate string has not been set, thereby returning " 
							+ "a blank space as default."
							);
				}
				return " ";
			}
			return concatinateStr;
		}
		/**
		 * Set string being used as the end delimiter to concatenate split arguments to single line. 
		 * @param concatinateStr
		 */
		public void setConcatinateStr( String concatinateStr) {
			this.concatinateStr = concatinateStr;
		}
		
	/**
	 * Strip down CR, LF, TAB characters and surplus white spaces from multilineArgs. 
	 * @param multilineArgs : concatenated multiple string arguments contains CR, LF, TAB characters, 
	 * and surplus white spaces.
	 * @return single string after stripped down CR, LF, TAB characters, and surplus white spaces. 
	 */
	public String concatinate( final String multilineArgs) {
		if ( multilineArgs == null) {
			throw new IllegalArgumentException( "multilineArgs input cannot be null.");
		}
		if ( multilineArgs.length() < 1) {
			Logger loggerObj = getLogger();
				if ( loggerObj.isInfoEnabled()) {
					loggerObj.info( 
							"Case of irregular input: value of multilineArgs input is empty string"
							);
				}
				
			return "";
		}
		
		String concatinatedArgsStr = "";
			String concatinateStr = getConcatinateStr();
			for( String argStr : splitByPrefix( multilineArgs)) {
				concatinatedArgsStr = concatinatedArgsStr + concatinateStr + argStr;
			}
		return concatinatedArgsStr.substring( concatinateStr.length());
	}
}
